Skip to content

Noise floor and local stats log#1566

Merged
garthvh merged 11 commits into
mainfrom
noise-floor
Jun 3, 2026
Merged

Noise floor and local stats log#1566
garthvh merged 11 commits into
mainfrom
noise-floor

Conversation

@RCGV1

@RCGV1 RCGV1 commented Jan 18, 2026

Copy link
Copy Markdown
Member

What changed?

  • Added a Local Stats Log page in Node Details showing a time-series chart of noise floor (dBm) with a red threshold line at −85 dBm, plus a scrollable table of per-reading stats (relayed, canceled, dupes, packets tx/rx, nodes online/total) with CSV export and clear-log actions.
  • Added noise floor readings (stored as Int32?) to the SwiftData TelemetryEntity model with the required VersionedSchema migration entry.
  • Added a Request Local Stats button (30 s cooldown via RateLimitedButton) in Node Details for requesting local stats from a remote node.
  • Added a TipKit tip in LocalStatsLog explaining that noise floor readings are directional diagnostics that can vary quickly, and that external filters may lower the displayed value due to insertion loss or in-band interference.
  • Noise floor display is gated to firmware that populates the field: firmware predating noise floor support sends the proto3 default 0; the app stores that as nil and shows "No Reading" instead of 0 dBm, since real LoRa noise floors are always negative.
  • Replaced slang chart label "Icky" with "Threshold (-85 dBm)".
  • Fixed alert typo: "Responses can take some time.".
  • Added user documentation in docs/user/nodes.md and docs/user/telemetry.md with bundled in-app HTML regenerated.
  • Added CSV regression test (LocalStatsTelemetryExportTests) covering noise floor export.

Why did it change?

Local stats and noise floor are high-value RF diagnostics for advanced users monitoring mesh health and receiver conditions. The noiseFloor field (field 15 in LocalStats) is already present in the official protobufs on main via firmware PR #9347.

How is this tested?

Tested with custom firmware. Localizable.xcstrings validated with xcrun xcstringstool compile --dry-run. Simulator app build passed with CODE_SIGNING_ALLOWED=NO. LocalStatsTelemetryExportTests build-for-testing passed.

Screenshots/Videos (when applicable)

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly (see copilot-instructions.md for the view → doc page mapping). If no doc update is needed, add the skip-docs-check label.
  • I have tested the change to ensure that it works as intended.

RCGV1 added 3 commits January 17, 2026 19:30
Conflicts resolved:
- AccessoryManager+ToRadio.swift: Keep both sendLocalStatsRequest and exchangeUserInfo
- NodeDetail.swift: Keep both RequestLocalStatsButton and ExchangeUserInfoButton
- project.pbxproj: Add ExchangeUserInfoButton.swift to project
@garthvh garthvh requested a review from GUVWAF February 12, 2026 00:00
@GUVWAF

GUVWAF commented Feb 12, 2026

Copy link
Copy Markdown
Member

Left some comments in the firmware PR, but in general I would say it would be good to have an explanation with e.g. TipKit about it. It needs to be taken with a grain of salt, and it can vary quite a bit (as you can see in 2 minutes there is already almost 10dB difference). Adding filters does not necessarily help, e.g. when the noise/interference is in-band and may skew the result, as they have an insertion loss which will result in a lower noise floor shown.

@github-actions github-actions Bot added the Stale label Mar 15, 2026
@github-actions github-actions Bot closed this Mar 23, 2026
@thebentern thebentern reopened this Mar 23, 2026
@thebentern thebentern removed the Stale label Mar 23, 2026
@Deuteranomalous1

Copy link
Copy Markdown

Great feature that will really assist with infrastructure site selection. No more inferring noise floor via traceroute to other nodes.

@garthvh

garthvh commented Apr 18, 2026

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

@garthvh

garthvh commented Apr 18, 2026

Copy link
Copy Markdown
Member

Left some comments in the firmware PR, but in general I would say it would be good to have an explanation with e.g. TipKit about it. It needs to be taken with a grain of salt, and it can vary quite a bit (as you can see in 2 minutes there is already almost 10dB difference). Adding filters does not necessarily help, e.g. when the noise/interference is in-band and may skew the result, as they have an insertion loss which will result in a lower noise floor shown.

@copilot implement this tipkit suggestion

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
@CLAassistant

CLAassistant commented Apr 18, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ RCGV1
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged origin/main into the branch and resolved the two conflicts in commit 436ba7a0:

  • Meshtastic.xcodeproj/project.pbxproj: Took main's version — updated MARKETING_VERSION to 2.7.9 and added OTHER_LDFLAGS with the SwiftUI weak framework link.
  • Meshtastic/Helpers/MeshPackets.swift: Took main's refactored async nodeInfoPacket (returns NSManagedObjectID? via context.perform) and async telemetryPacket, while preserving the noise floor additions (telemetry.noiseFloor = telemetryMessage.localStats.noiseFloor and the updated log message).

Copilot AI requested a review from garthvh April 18, 2026 23:20

@garthvh garthvh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR — noise floor and local stats are genuinely useful diagnostics for advanced users, and the overall structure of the feature (chart + table + CSV export + request button with cooldown) is solid and matches the pattern of the existing telemetry log views. A few blocking issues need to be resolved before this can merge.


Critical — .gitmodules points to a forked protobufs submodule

The PR changes .gitmodules to point at https://github.com/RCGV1/protobufs-fork.git on branch noise-floor. This cannot be merged — the submodule must always point to https://github.com/meshtastic/protobufs.git.

The good news: noiseFloor (field 15 in LocalStats) is already present in the official protobufs on maintelemetryMessage.localStats.noiseFloor is already available as Int32. Please revert the .gitmodules change and point the submodule back to the official repo.

Critical — Wrong persistence layer for TelemetryEntity

The PR modifies Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel and TelemetryEntity+CoreDataClass.swift. Core Data has been replaced by SwiftData in this project. On main, TelemetryEntity is a SwiftData @Model at Meshtastic/Model/TelemetryEntity.swift.

The noiseFloor property must be added to the SwiftData model:

// Meshtastic/Model/TelemetryEntity.swift
var noiseFloor: Int32?

A VersionedSchema migration entry is also required in MeshtasticSchema.swift. See docs/developer/swiftdata.md for the migration pattern used in this project.

Critical — NSPredicate in LocalStatsLog and NodeDetail

NSPredicate is a Core Data API. Two places use it:

// LocalStatsLog.swift
node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4"))

// NodeDetail.swift
node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4")).count

With SwiftData, use Swift's native filter:

node.telemetries.filter { $0.metricsType == 4 }

Critical — @Environment(.managedObjectContext) in LocalStatsLog

@Environment(\.managedObjectContext) var context

This is the Core Data environment key. The SwiftData equivalent is:

@Environment(\.modelContext) var context

Warning — Proto type mismatch: noiseFloor is Int32, not Float

The protobuf defines noiseFloor as Int32 (whole dBm value). The PR stores and displays it as Float. Please keep it as Int32 throughout — in the SwiftData model, the CSV export, and the chart/table display. The noiseFloor != 0 nil check should also become hasValue optional pattern once stored as Int32?.

Warning — Typo in user-facing alert string

"Responses can some time."

Should be:

"Responses can take some time."

Warning — "Icky" as a localizable string

"Icky" is used as a RuleMark chart label and appears in Localizable.xcstrings with the comment "Icky" is slang for very bad. Slang does not translate well and will confuse translators. Please use a proper technical label such as "Poor Signal" or "Threshold (-85 dBm)".

Warning — Checklist not completed

All checklist items are unchecked. Please complete the self-review checklist before requesting review.


What to keep as-is

  • sendLocalStatsRequest() in AccessoryManager+ToRadio — clean, follows existing patterns perfectly
  • RateLimitedButton with 30s cooldown — correct approach
  • CSV export in WriteCsvFile.swift — well structured
  • Overall LocalStatsLog view structure (chart + table + export buttons) — matches DeviceMetricsLog and other telemetry log views nicely
  • noiseFloor colour coding logic — sensible thresholds

@RCGV1

RCGV1 commented May 29, 2026

Copy link
Copy Markdown
Member Author

Updated this branch with the review fixes.

Changes made:

  • Merged current upstream/main into noise-floor and resolved the dirty merge state locally.
  • Restored .gitmodules to the official https://github.com/meshtastic/protobufs.git submodule URL.
  • Moved noiseFloor persistence to the SwiftData TelemetryEntity as Int32? and added the SwiftData schema/migration wiring.
  • Removed the Core Data NSPredicate / managedObjectContext usage from the local stats flow.
  • Kept noiseFloor as optional Int32 through storage, display, and CSV export, including preserving 0 as a real reading instead of treating it as missing.
  • Added the TipKit caveat for directional/variable noise-floor readings and filter insertion-loss caveats.
  • Replaced the Icky chart label with Threshold (-85 dBm) and fixed the alert typo to Responses can take some time.
  • Added backup restore handling for noiseFloor and a CSV regression test covering 0 dBm export.

Validation performed:

  • xcrun xcstringstool compile --dry-run passed for Localizable.xcstrings.
  • Simulator app build passed with CODE_SIGNING_ALLOWED=NO.
  • LocalStatsTelemetryExportTests build-for-testing passed.
  • Full simulator test execution hung in the runner after build-for-testing, so I did not get a final XCTest pass/fail result from that run.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

✅ Docs staleness check passed

This PR includes updates to docs/ alongside the source changes. Thank you!

@RCGV1

RCGV1 commented May 29, 2026

Copy link
Copy Markdown
Member Author

Follow-up after the docs-staleness warning: added user docs for Local Stats and noise floor in docs/user/nodes.md and docs/user/telemetry.md, then regenerated the bundled in-app docs with bash scripts/build-docs.sh --output Meshtastic/Resources/docs --beta. Pushed in bbfe0118.

@RCGV1 RCGV1 requested a review from garthvh May 29, 2026 00:12
@garthvh

garthvh commented Jun 3, 2026

Copy link
Copy Markdown
Member

@copilot is this functionality properly gated to only the firmware versions where the readings are available?

Firmware that predates noiseFloor support sends the proto3 default of 0
for LocalStats.noiseFloor. Real LoRa noise floors are always negative
(-80 to -120 dBm), so storing 0 directly would show "0 dBm" in the UI
for every older node instead of "No Reading".

Treat the proto3 default as nil at storage time so that noise floor
display is naturally gated to firmware versions that actually populate
the field (non-zero value).

Copilot AI commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@copilot is this functionality properly gated to only the firmware versions where the readings are available?

Not fully — fixed in a6be7c9.

The noiseFloor field in LocalStats is proto3, so firmware that predates support for it (pre-firmware-PR #9347) sends the wire default 0. The code was storing that 0 directly into the SwiftData Int32? field, which means older nodes would display 0 dBm in the chart and table instead of "No Reading".

Since LoRa noise floors are always negative (−80 to −120 dBm), 0 is an impossible real reading and unambiguously means "not populated by this firmware." The fix treats the proto3 default as nil at storage time:

telemetry.noiseFloor = telemetryMessage.localStats.noiseFloor != 0 ? telemetryMessage.localStats.noiseFloor : nil
  • Firmware with noise floor support → returns a non-zero negative value → stored and displayed normally.
  • Older firmware → returns 0 → stored as nil → shows "No Reading" in the table and is excluded from the chart.

The RequestLocalStatsButton doesn't need version gating: it uses the standard TELEMETRY_APP wantResponse mechanism that all firmware versions support — older nodes simply respond with local stats packets that don't include a noise floor reading.

Copilot AI changed the title Noise floor Noise floor and local stats log Jun 3, 2026
garthvh and others added 4 commits June 3, 2026 09:00
# Conflicts:
#	Meshtastic.xcodeproj/project.pbxproj
#	Meshtastic/Helpers/MeshPackets.swift
#	Meshtastic/Resources/docs/index.json
#	Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
The noise-floor branch introduced MeshtasticSchemaV2 and a V1→V2 lightweight
migration for the local-stats fields. Since V1 has not shipped, there are no V1
stores to migrate from — the migration is unnecessary risk. The local-stats
fields already live in the @model classes that V1 references, so fold the change
into V1 and remove V2.

- MeshtasticSchema: current/allModels back to V1 (also restores DeviceLinkEntity,
  which V2's model list omitted)
- MeshtasticMigrationPlan: V1-only, no stages
- Delete MeshtasticSchemaV2.swift and its project.pbxproj entries

Builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The waypoint-editing refactor in WaypointForm.swift is unrelated to the noise
floor / local stats feature. Restore it to match main so this PR is scoped to
the feature; the refactor can land in its own PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
noise_floor is a plain proto3 scalar with no presence tracking, so unset and a
literal 0 are indistinguishable on the wire. Document why 0 is treated as nil
and what would be required (upstream optional field) for true presence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@garthvh garthvh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working good

@garthvh garthvh merged commit b527c89 into main Jun 3, 2026
4 checks passed
@garthvh garthvh deleted the noise-floor branch June 3, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants